namespace ScrumSprintMonitor.Wpf.Application
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.ComponentModel.Composition;
	using System.ComponentModel.Composition.Hosting;
	using System.Diagnostics;
	using System.Globalization;
	using System.IO;
	using System.Linq;
	using System.Reflection;
	using System.Windows;
	using System.Windows.Forms;
	using System.Windows.Threading;
	using System.Xml;

	using Extensibility;
	using Extensibility.Events;

	using Implementation;

	using Infrastructure;
	using Infrastructure.Configuration;

	using log4net.Config;

	using Microsoft.Practices.Composite.Events;
	using Microsoft.Practices.Composite.Logging;
	using Microsoft.Practices.Composite.Modularity;
	using Microsoft.Practices.Composite.Presentation.Regions;
	using Microsoft.Practices.Composite.Regions;
	using Microsoft.Practices.ServiceLocation;
	using Microsoft.SDK.Samples.VistaBridge.Services;

	using Ninject.Core;

	using NinjectContrib.CompositePresentation;

	using Properties;

	using UI.Wpf;
	using UI.Wpf.Avatars;

	#endregion

	public class Bootstrapper : NinjectBootstrapper
	{
		private readonly Assembly assembly = Assembly.GetExecutingAssembly();
		private CompositionContainer container;
		private IEventAggregator eventAggregator;
		private ILogger<App> logger;
		private ILoggerFacade loggerFacade;
		private NinjectContainerAbstraction ninjectContainerAbstraction;
		private Window primaryMonitorShell;

		public Bootstrapper()
		{
			if (this.assembly != null)
			{
				Debug = this.assembly.Location.ToUpperInvariant().EndsWith("EXE");
			}

			Infrastructure.DesignerProperties.GetIsInDesignMode = (object x) => StubDesignerProperties.GetIsInDesignMode(null);

			System.Windows.Application.Current.DispatcherUnhandledException += Application_DispatcherUnhandledException;
			System.Windows.Application.Current.Exit += Application_Exit;

			Configlog4net();
		}

		[ImportMany(typeof(ScrumSprintMonitorAddIn))]
		public List<ScrumSprintMonitorAddIn> AddIns { get; set; }

		[ImportMany(typeof(Ninject.Core.IModule))]
		public List<Ninject.Core.IModule> AdditionalKernelModules { get; set; }

		internal static bool Debug { get; set; }

		protected override ILoggerFacade LoggerFacade
		{
			get
			{
				if (this.loggerFacade == null)
				{
					this.loggerFacade = new Log4NetLoggerFacade();
				}

				return this.loggerFacade;
			}
		}

		private void Configlog4net()
		{
#if DEBUG
			const string logConfigFileName = "log4net.debug.config.xml";
			string initialLogConfig = Resources.log4net_debug_config;
#else
			const string logConfigFileName = "log4net.release.config.xml";
			string initialLogConfig = Resources.log4net_release_config;
#endif
			const string logFileBaseName = "scrumsprintmonitor.log";
			string assemblyLocation = this.assembly.Location;
			var versionInfo = FileVersionInfo.GetVersionInfo(assemblyLocation);
			string appDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
			                                    versionInfo.CompanyName.Replace(' ', '_'));
			string configFilePath = Path.Combine(appDataFolder, logConfigFileName);

			//when the config file changes and you need to implement a new one, 
			//   uncomment this so it deletes the current one so it will copy over a new one 
			//if (File.Exists(configFilePath))
			//{
			//    try
			//    {
			//        File.SetAttributes(configFilePath, FileAttributes.Normal);
			//        File.Delete(configFilePath);
			//    }
			//    catch
			//    {
			//        //don't do anything, just let it go
			//    }
			//}

			var configFile = new FileInfo(configFilePath);

			if (!configFile.Exists)
			{
				Directory.CreateDirectory(Path.GetDirectoryName(configFilePath));

				//if they don't already have a config file, go find it and copy it 
				using (var writer = File.CreateText(configFile.FullName))
				{
					var document = new XmlDocument();
					document.LoadXml(initialLogConfig);

					foreach (XmlNode node in document.GetElementsByTagName("file"))
					{
						node.Attributes.GetNamedItem("value").Value = Path.Combine(appDataFolder, logFileBaseName);
						break;
					}

					document.Save(writer);
				}
			}
			else
			{
				var document = new XmlDocument();
				document.Load(configFilePath);

				foreach (XmlNode node in document.GetElementsByTagName("file"))
				{
					string existingLogFilePath = node.Attributes.GetNamedItem("value").Value;
					string logFilePath = Path.Combine(appDataFolder, logFileBaseName);
					if (logFilePath != existingLogFilePath)
					{
						node.Attributes.GetNamedItem("value").Value = logFilePath;

						document.Save(configFilePath);
					}
					break;
				}
			}

			XmlConfigurator.Configure(configFile);
		}

		private bool Compose()
		{
			this.logger.Debug("Composing MEF container...");

			var aggregateCatalog = new AggregateCatalog();
			string binFolder = Path.GetDirectoryName(this.assembly.Location);
			string addInsBinFolder = Path.Combine(binFolder, @"AddIns");
			string serverAdaptersBinFolder = Path.Combine(binFolder, @"AddIns\ServerAdapters");

			aggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(WpfUriAvatar).Assembly));
			AddCatalogFromFolderPath(aggregateCatalog, serverAdaptersBinFolder);
			AddCatalogFromFolderPath(aggregateCatalog, addInsBinFolder);

			this.logger.Debug("Found {0} parts in {1} MEF catalogs:", aggregateCatalog.Parts.Count(), aggregateCatalog.Catalogs.Count);
			System.Diagnostics.Debug.Assert(aggregateCatalog.Parts.Any());

			this.container = new CompositionContainer(aggregateCatalog);

			var batch = new CompositionBatch();
			batch.AddPart(this);
			batch.AddPart(Kernel.Get<IProcessTemplateAdapterProvider>());
			batch.AddPart(Kernel.Get<IAvatarProviderStore>());
			batch.AddExportedValue(ServiceLocator.Current.GetInstance<IServiceProvider>());
			batch.AddExportedValue<ICompositionService>(this.container);

			try
			{
				this.container.Compose(batch);
			}
			catch (CompositionException compositionException)
			{
				this.logger.Fatal(compositionException, "Found exception while composing MEF container.");

				System.Windows.MessageBox.Show(compositionException.ToString());
				return false;
			}

			return true;
		}

		private void AddCatalogFromFolderPath(AggregateCatalog aggregateCatalog, string binFolder)
		{
			if (Directory.Exists(binFolder))
			{
				this.logger.Debug("Creating MEF DirectoryCatalog from {0}...", binFolder);

				var addinsDirectoryCatalog = new DirectoryCatalog(binFolder);
				aggregateCatalog.Catalogs.Add(addinsDirectoryCatalog);

				foreach (var part in addinsDirectoryCatalog.Parts)
				{
					this.logger.Debug("{0}", part.ToString());
				}
			}
			else
			{
				this.logger.Debug("{0} addin folder not found.", binFolder);
			}
		}

		private void ProcessCommandLine()
		{
			try
			{
				var args = Environment.GetCommandLineArgs();
				if (args.Length > 1 && !string.IsNullOrEmpty(Settings.Default.TfsServerName))
				{
					// Get the 2 character command line argument
					string arg = args[1].ToLower(CultureInfo.InvariantCulture).Trim().Substring(0, 2);
					switch (arg)
					{
						case "/c":
							// Show the options dialog
							ConfigureScreensaver();
							break;
						case "/p":
							// Don't do anything for preview
							System.Windows.Application.Current.Shutdown();
							break;
						case "/s":
							// Show screensaver form
							ShowScreensaver();
							break;
						default:
							System.Windows.Application.Current.Shutdown();
							break;
					}
				}
				else
				{
					if (Debug)
					{
						System.Windows.Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;

						// If no arguments were passed in, configure the screensaver
						if (ConfigureScreensaver())
						{
							Settings.Default.Refresh();

							ShowScreensaver();
						}
						else
						{
							System.Windows.Application.Current.Shutdown();
						}
					}
					else
					{
						// If no arguments were passed in, configure the screensaver
						ConfigureScreensaver();
					}
				}
			}
			catch (IOException ioException)
			{
				this.logger.Error(ioException, "Exception found while starting app.");

				System.Windows.MessageBox.Show(ioException.Message);

				System.Windows.Application.Current.Shutdown();

				this.primaryMonitorShell = null;
			}
		}

		private bool ConfigureScreensaver()
		{
			bool isTopMost = false;

			if (this.primaryMonitorShell != null)
			{
				isTopMost = this.primaryMonitorShell.Topmost;

				this.primaryMonitorShell.Topmost = false;
			}

			string binFolderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
			string configurationEditorExe = Path.Combine(binFolderPath, "ScrumSprintMonitor.ConfigEditor.exe");

			try
			{
				int exitCode = UACManager.ExecutePrivilegedProcess(configurationEditorExe);
				return (exitCode == 1);
			}
			catch (Win32Exception ex)
			{
				System.Diagnostics.Debug.Assert(ex.NativeErrorCode == 1223); // E_ABORT
				return false;
			}
			finally
			{
				if (isTopMost)
				{
					this.primaryMonitorShell.Topmost = isTopMost;
				}
			}
		}

		/// <summary>
		/// Shows screen saver by creating one instance of MainWindow for each monitor.
		/// 
		/// Note: uses WinForms's Screen class to get monitor info.
		/// </summary>
		private Window ShowScreensaver()
		{
			NinjectContainerAbstraction.EnsureConfigurationDependentModulesAreSetUp();

			this.logger.Info("Starting screensaver...");

			var monitorStartedEvent = this.eventAggregator.GetEvent<MonitorStartedEvent>();

			monitorStartedEvent.Publish(EventArgs.Empty);

			this.eventAggregator.GetEvent<InvokeConfigurationUiEvent>().Subscribe(OnInvokeConfigurationUI);

			bool multiMonitor = ServiceLocator.Current.GetInstance<IShellLayoutConfiguration>().MultiMonitor;

			//creates window on primary screen
			var primaryShell = Kernel.Get<PrimaryMonitorShell>();
			primaryShell.WindowStartupLocation = WindowStartupLocation.Manual;
			if (Debug)
			{
				primaryShell.WindowStyle = WindowStyle.ToolWindow;
				primaryShell.ShowInTaskbar = true;
			}

			this.primaryMonitorShell = primaryShell;

			ShowMonitorWindow(primaryShell);

			if (multiMonitor)
			{
				var secondaryMonitor = Screen.AllScreens[1 % Screen.AllScreens.Length];

				var secondaryShell = Kernel.Get<SecondaryMonitorShell>();
				secondaryShell.WindowStartupLocation = WindowStartupLocation.Manual;
				var location = secondaryMonitor.Bounds;
				if (Debug)
				{
					secondaryShell.WindowStyle = WindowStyle.ToolWindow;
					secondaryShell.ShowInTaskbar = true;
				}

				if (secondaryShell.WindowStyle == WindowStyle.ToolWindow)
				{
					//covers entire monitor
					secondaryShell.Left = location.X - 7;
					secondaryShell.Top = location.Y - 7;
					secondaryShell.Width = location.Width + 14;
					secondaryShell.Height = location.Height + 14;
				}
				else
				{
					//covers entire monitor
					secondaryShell.Left = location.X;
					secondaryShell.Top = location.Y;
					secondaryShell.Width = location.Width;
					secondaryShell.Height = location.Height;
				}

				//show non-primary screen windows
				ShowMonitorWindow(secondaryShell);

				RegionManager.SetRegionManager(secondaryShell, Kernel.Get<IRegionManager>());
				RegionManager.UpdateRegions();
			}

			System.Windows.Application.Current.MainWindow = primaryShell;
			System.Windows.Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;

			this.logger.Info(multiMonitor ? "Started screensaver in multi-monitor mode." : "Started screensaver in single monitor mode.", Category.Info,
			                 Priority.Low);

			return primaryShell;
		}

		private void ShowMonitorWindow(Window window)
		{
			window.ShowActivated = true;
			window.Show();

			var monitorStartedEvent = this.eventAggregator.GetEvent<MonitorWindowShownEvent>();

			monitorStartedEvent.Publish(window);
		}

		private void LoadAddIn(ScrumSprintMonitorAddIn addIn)
		{
			this.logger.Info("Loading add-in {0}...", addIn.GetType().Name);

			addIn.Load();

			this.logger.Info("Loaded add-in {0}.", addIn.GetType().Name);
		}

		private void UnloadAddIn(ScrumSprintMonitorAddIn addIn)
		{
			this.logger.Info("Unloading add-in {0}...", addIn.GetType().Name);

			var disposable = addIn as IDisposable;
			addIn.Unload();
			if (disposable != null)
			{
				disposable.Dispose();
			}

			this.logger.Info("Unloaded add-in {0}.", addIn.GetType().Name);
		}

		private void UnloadAddIns()
		{
			AddIns.ForEach(UnloadAddIn);
			AddIns.Clear();
		}

		private void Application_Exit(object sender, ExitEventArgs e)
		{
			this.eventAggregator.GetEvent<InvokeConfigurationUiEvent>().Unsubscribe(OnInvokeConfigurationUI);

			if (this.primaryMonitorShell != null)
			{
				this.logger.Info("Shutting down screensaver...");
			}

			UnloadAddIns();

			if (this.container != null)
			{
				this.container.Dispose();
			}

			//this.ninjectContainerAbstraction.Dispose();
		}

		private void OnInvokeConfigurationUI(Window obj)
		{
			ConfigureScreensaver();
			Settings.Default.Refresh();
		}

		private void Application_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
		{
			if (this.logger != null)
			{
				this.logger.Fatal(e.Exception, "Unhandled exception from {0}", sender);
			}
		}

		#region Overrides of NinjectBootstrapper

		protected override IKernel CreateKernel()
		{
			this.ninjectContainerAbstraction = NinjectContainerAbstraction.EnsureContainerIsSetUp();

			return this.ninjectContainerAbstraction.Instance;
		}

		protected override void ConfigureKernel()
		{
			base.ConfigureKernel();

			Kernel.Load(new InlineModule(m => m.Bind<PrimaryMonitorShell>().ToSelf(), m => m.Bind<SecondaryMonitorShell>().ToSelf()));

			NinjectContainerAbstraction.RegisterModule<NinjectAppRuntimeModule>();
			NinjectContainerAbstraction.RegisterModule<NinjectAppConfigurationDependentModule>(true);
			this.ninjectContainerAbstraction.LoadRegisteredModules(false);

			var versionInfo = FileVersionInfo.GetVersionInfo(this.assembly.Location);

			this.eventAggregator = Kernel.Get<IEventAggregator>();
			this.logger = Kernel.Get<ILogger<App>>();
			this.logger.Info("Application version {0}.", versionInfo.FileVersion);

			if (Compose())
			{
				Kernel.Load(AdditionalKernelModules);

				AddIns.ForEach(LoadAddIn);
			}
			else
			{
				System.Windows.Application.Current.Shutdown();
			}
		}

		protected override IModuleCatalog GetModuleCatalog()
		{
			return new DirectoryModuleCatalog
			       	{
			       		ModulePath = Path.GetDirectoryName(this.assembly.Location)
			       	};
		}

		protected override DependencyObject CreateShell()
		{
			ProcessCommandLine();

			return this.primaryMonitorShell;
		}

		protected override void InitializeModules()
		{
			if (this.primaryMonitorShell == null)
			{
				return;
			}

			base.InitializeModules();
		}

		#endregion
	}
}